Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit fc2f7708644509448a83121bbb477e2372ed88bf


Parents : 1e440c5
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-08T10:06:14-05:00

feat(WebSocketConnection): implement foreground and network change handling for improved reconnection logic

Changes

2 files changed, 89 insertions(+), 0 deletions(-)


Diff

diff --git a/meshchatx/src/frontend/js/WebSocketConnection.js b/meshchatx/src/frontend/js/WebSocketConnection.js
index 6cf765b1..003d7968 100644
--- a/meshchatx/src/frontend/js/WebSocketConnection.js
+++ b/meshchatx/src/frontend/js/WebSocketConnection.js
@@ -19,6 +19,9 @@ class WebSocketConnection {
this.destroyed = false;
this._hadSuccessfulOpen = false;
this._pendingReconnectUi = false;
+ this._lastReceivedTime = Date.now();
+ this._hasEventListeners = false;
+ this._isForcedReconnect = false;
}
async connect() {
@@ -30,6 +33,24 @@ class WebSocketConnection {
}
this.initialized = true;
+
+ if (typeof window !== "undefined" && window.addEventListener) {
+ if (!this._hasEventListeners) {
+ window.addEventListener("visibilitychange", () => {
+ if (typeof document !== "undefined" && document.visibilityState === "visible") {
+ this.handleForegroundOrNetworkChange();
+ }
+ });
+ window.addEventListener("focus", () => {
+ this.handleForegroundOrNetworkChange();
+ });
+ window.addEventListener("online", () => {
+ this.handleForegroundOrNetworkChange();
+ });
+ this._hasEventListeners = true;
+ }
+ }
+
this.reconnect();
}
@@ -130,6 +151,7 @@ class WebSocketConnection {
const isReconnect = this._pendingReconnectUi;
this._pendingReconnectUi = false;
this._hadSuccessfulOpen = true;
+ this._lastReceivedTime = Date.now();
this.emit("connected", { isReconnect });
});
@@ -138,6 +160,10 @@ class WebSocketConnection {
if (this.destroyed) {
return;
}
+ if (this._isForcedReconnect) {
+ this._isForcedReconnect = false;
+ return;
+ }
if (this._hadSuccessfulOpen) {
this._pendingReconnectUi = true;
}
@@ -165,6 +191,7 @@ class WebSocketConnection {
});
this.ws.onmessage = (message) => {
+ this._lastReceivedTime = Date.now();
try {
const data = JSON.parse(message.data);
if (data && data.type === "pong") {
@@ -178,6 +205,37 @@ class WebSocketConnection {
};
}
+ handleForegroundOrNetworkChange() {
+ if (!this.initialized || this.destroyed) {
+ return;
+ }
+
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
+ this.reconnect();
+ return;
+ }
+
+ const idleTime = Date.now() - this._lastReceivedTime;
+ if (idleTime > PING_INTERVAL_MS) {
+ this.forceReconnect();
+ } else {
+ this._sendAppPing();
+ }
+ }
+
+ forceReconnect() {
+ if (this.ws) {
+ this._isForcedReconnect = true;
+ try {
+ this.ws.close();
+ } catch {
+ // ignore
+ }
+ this.ws = null;
+ }
+ this.reconnect();
+ }
+
destroy() {
this.destroyed = true;
this.initialized = false;

diff --git a/tests/frontend/WebSocketConnection.test.js b/tests/frontend/WebSocketConnection.test.js
index fd0fe8e4..178469ed 100644
--- a/tests/frontend/WebSocketConnection.test.js
+++ b/tests/frontend/WebSocketConnection.test.js
@@ -133,4 +133,35 @@ describe("WebSocketConnection module", () => {
WebSocketConnection.destroy();
});
+
+ it("reconnects or pings on foreground or network change", async () => {
+ const MockWS = makeWsImpl();
+ global.WebSocket = MockWS;
+
+ const { default: WebSocketConnection } = await import("../../meshchatx/src/frontend/js/WebSocketConnection.js");
+
+ await WebSocketConnection.connect();
+ await vi.waitUntil(() => WebSocketConnection.ws?.readyState === MockWS.OPEN);
+
+ const firstWs = WebSocketConnection.ws;
+ const sendSpy = vi.spyOn(firstWs, "send");
+
+ // 1. If idleTime is small, it should just send a ping
+ WebSocketConnection._lastReceivedTime = Date.now();
+ WebSocketConnection.handleForegroundOrNetworkChange();
+ expect(sendSpy).toHaveBeenCalled();
+ expect(WebSocketConnection.ws).toBe(firstWs);
+
+ // 2. If idleTime is large, it should force a reconnect
+ WebSocketConnection._lastReceivedTime = Date.now() - 60000;
+ WebSocketConnection.handleForegroundOrNetworkChange();
+
+ // Wait for the new WebSocket to be created and opened
+ await vi.waitUntil(() => WebSocketConnection.ws && WebSocketConnection.ws !== firstWs);
+ await vi.waitUntil(() => WebSocketConnection.ws.readyState === MockWS.OPEN);
+
+ expect(WebSocketConnection.ws).not.toBe(firstWs);
+
+ WebSocketConnection.destroy();
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────